package com.springboot.demo.design.pattern.processor;

import com.springboot.demo.design.pattern.plugin.AfterPlugin;
import com.springboot.demo.design.pattern.plugin.AfterPluginDefine;
import com.springboot.demo.design.pattern.plugin.FrontPlugin;
import com.springboot.demo.design.pattern.plugin.FrontPluginDefine;
import com.springboot.demo.design.pattern.processor.impl.UserProcessor;
import com.springboot.demo.util.ContextWareUtil;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;
import lombok.extern.log4j.Log4j2;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.CollectionUtils;

/**
 * 自定义invocationHandler
 *
 * @author supu
 * @date 2019-03-19 16:47
 **/
@Log4j2
public class CustomInvocationHandler implements InvocationHandler {
    private Object target;
    private final TransactionTemplate transactionTemplate;
    private final Map<Method, List<FrontPlugin>> frontPluginMap;
    private final Map<Method, List<AfterPlugin>> afterPluginMap;

    private final ReentrantLock reentrantLock = new ReentrantLock();

    public CustomInvocationHandler(Object target, PlatformTransactionManager transactionManager) {
        this.target = target;
        frontPluginMap = new HashMap<>(16);
        afterPluginMap = new HashMap<>(16);
        transactionTemplate = new TransactionTemplate(transactionManager);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (method.getDeclaringClass().equals(UserProcessor.class)) {
            return invokePluginMethod(proxy, method, args);
        }
        return method.invoke(target, args);
    }

    private Object invokePluginMethod(Object proxy, Method method, Object[] args) {
        Throwable throwable = null;
        Object result = null;
        try {
            doFrontPlugin(proxy, method, args);

            if (method.getAnnotation(Transactional.class) != null) {
                // 有事务注解
                result = doTransactionInvoke(proxy, method, args);
            } else {
                result = method.invoke(target, args);
            }

        } catch (Exception e) {
            throwable = e;
            log.error("method:{} 前置插件运行异常：{}", method.getName(), e);
        } finally {
            doAfterPlugin(proxy, method, args, result, throwable);
        }
        return result;
    }

    private Object doTransactionInvoke(Object proxy, Method method, Object[] args) {
        return transactionTemplate.execute((transactionStatus) -> {
            try {
                return method.invoke(target, args);
            } catch (IllegalAccessException | InvocationTargetException e) {
                log.error("method:{} 事务处理异常：{}", e);
                transactionStatus.setRollbackOnly();
            }
            return null;
        });
    }

    private void doAfterPlugin(Object proxy, Method method, Object[] args, Object result, Throwable throwable) {
        List<AfterPlugin> afterPluginList = afterPluginMap.get(method);
        if (CollectionUtils.isEmpty(afterPluginList)) {
            try {
                reentrantLock.lock();
                afterPluginList = afterPluginMap.get(method);
                if (CollectionUtils.isEmpty(afterPluginList)) {
                    afterPluginList = parseAfterPlugin(method);
                    afterPluginMap.put(method, afterPluginList);
                }
            } catch (Exception e) {
                log.error("method:{} 后置插件获取异常：{}", method.getName(), e);
            } finally {
                reentrantLock.unlock();
            }
        }

        if (!CollectionUtils.isEmpty(afterPluginList)) {
            afterPluginList.forEach(afterPlugin -> afterPlugin.interceptor(proxy, method, args, result, throwable));
        }
    }

    private List<AfterPlugin> parseAfterPlugin(Method method) {
        AfterPluginDefine afterPluginDefine = method.getAnnotation(AfterPluginDefine.class);
        if (afterPluginDefine == null) {
            return null;
        }
        Class<?>[] classes = afterPluginDefine.value();
        List<AfterPlugin> afterPluginList = new ArrayList<>(classes.length);

        for (Class afterPluginClass : classes) {
            AfterPlugin afterPlugin = (AfterPlugin) ContextWareUtil.getBean(afterPluginClass);
            afterPluginList.add(afterPlugin);
        }
        return afterPluginList;
    }

    private void doFrontPlugin(Object proxy, Method method, Object[] args) {
        List<FrontPlugin> frontPluginList = frontPluginMap.get(method);
        if (CollectionUtils.isEmpty(frontPluginList)) {
            try {
                reentrantLock.lock();
                frontPluginList = frontPluginMap.get(method);
                if (CollectionUtils.isEmpty(frontPluginList)) {
                    frontPluginList = parseFrontPlugin(method);
                    frontPluginMap.put(method, frontPluginList);
                }
            } catch (Exception e) {
                log.error("method:{} 前置插件获取异常：{}", method.getName(), e);
            } finally {
                reentrantLock.unlock();
            }
        }

        if (!CollectionUtils.isEmpty(frontPluginList)) {
            frontPluginList.forEach(frontPlugin -> frontPlugin.interceptor(proxy, method, args));
        }
    }

    private List<FrontPlugin> parseFrontPlugin(Method method) {
        FrontPluginDefine frontPluginDefine = method.getAnnotation(FrontPluginDefine.class);
        if (frontPluginDefine == null) {
            return null;
        }
        Class<?>[] classes = frontPluginDefine.value();
        List<FrontPlugin> frontPluginList = new ArrayList<>(classes.length);

        for (Class frontPluginClass : classes) {
            FrontPlugin frontPlugin = (FrontPlugin) ContextWareUtil.getBean(frontPluginClass);
            frontPluginList.add(frontPlugin);
        }
        return frontPluginList;
    }
}
